/*
 * Worker.java
 * 服务器端的工作线程
 */
import java.io.*;
import java.net.*;
import java.util.*;

public class Worker implements Runnable {
    public Worker(Socket soc)
    {
        this.soc = soc;
    }
    
    @Override
    public void run()
    {
        try {
            // 只和客户机主线程的 ObjectOutputStream 对应，都只 new 一次
            // 约定：向客户端发送对象，我的 ObjectOutputStream 每次都 new
            ObjectInputStream is = new ObjectInputStream(soc.getInputStream());
            
            String name = get_name(is);
            System.out.printf("[%s] 已加入会话。\n", name);
            while (!soc.isClosed()) {
                Object o = is.readObject();
                
                if (o instanceof End) {
                    System.out.printf("[%s] 退出了会话。\n", name);
                    ServerMain.soc_by_name.remove(name);
                    soc.close();
                    break;
                    
                } else if (o instanceof BroadcastMsg) {
                    broadcast((BroadcastMsg)o);
                    ObjectOutputStream os = new ObjectOutputStream(soc.getOutputStream());
                    os.writeObject(Stat.OK);
                    
                } else if (o instanceof PrivateMsg) {
                    Stat s = private_chat((PrivateMsg)o);
                    ObjectOutput os = new ObjectOutputStream(soc.getOutputStream());
                    os.writeObject(s);
                    
                } else {
                    ObjectOutputStream os = new ObjectOutputStream(soc.getOutputStream());
                    os.writeObject(Stat.UNKNOWN_CMD);
                }
            }
            
        } catch (IOException ex) {
            if (soc.isClosed()) {
                System.out.println("客户机已断开连接。");
            } else {
                ex.printStackTrace();
            }
        } catch (ClassNotFoundException ex) {
            ex.printStackTrace();
        }
    }
    
    /**
     * @return Stat.NOT_FOUND 找不到目标；Stat.OK；Stat.UNKNOWN_ERR。
     */
    Stat private_chat(PrivateMsg msg)
    {
        Socket s = ServerMain.soc_by_name.get(msg.get_reciever());
        if (s == null) {
            return Stat.NOT_FOUND;
        }
        if (s.isClosed()) {
            System.err.printf("[%s] 意外断开了连接，将删除套接字。\n", msg.get_reciever());
            ServerMain.soc_by_name.remove(msg.get_reciever());
            return Stat.NOT_FOUND;
        }
        
        try {
            new ObjectOutputStream(s.getOutputStream()).writeObject(msg);
            return Stat.OK;
        } catch (IOException ex) {
            ex.printStackTrace();
            return Stat.UNKNOWN_ERR;
        }
    }
    
    /**
     * 本函数会<strong>阻塞</strong>其它想修改 ServerMain.soc_by_name 的线程。
     * <p>
     * 把消息广播给除发送者之外的所有客户端。
     * </p>
     */
    void broadcast(BroadcastMsg msg)
    {
        Iterator<Map.Entry<String, Socket>> it = ServerMain.soc_by_name.entrySet().iterator();
        while (it.hasNext()) {
            Map.Entry<String, Socket> e = it.next();
            if (e.getKey().equals(msg.get_sender())) {
                continue;
            }
            
            Socket s = e.getValue();
            if (s.isClosed()) {
                System.err.printf("[%s] 意外断开了连接，将删除套接字。\n", e.getKey());
                it.remove();
                continue;
            }
            
            try {
                ObjectOutputStream os = new ObjectOutputStream(s.getOutputStream());
                os.writeObject(msg);
                
            } catch (IOException ex) {
                ex.printStackTrace();
            }
        }
    }
    
    /**
     * 本函数会<strong>阻塞</strong>在客户端的输入上，以及 ServerMain.soc_by_name
     * 的<strong>排他</strong>读写上。
     * <p>
     * 如果客户端输入的名字已经存在，则向客户端返回 Stat.DUP_NAME，并继续等待客户端输入。
     * </p>
     * 返回时已经建立了名字和套接字的映射。不会在服务器端显示信息。
     */
    String get_name(ObjectInputStream is) throws ClassNotFoundException, IOException
    {
        String name = null;
        ObjectOutputStream os = null;
        while (true) {
            name = (String)is.readObject();
            synchronized (ServerMain.soc_by_name) {
                if (!ServerMain.soc_by_name.containsKey(name)) {
                    ServerMain.soc_by_name.put(name, soc);
                    break;
                }
            }
            os = new ObjectOutputStream(soc.getOutputStream());
            os.writeObject(Stat.DUP_NAME);
        }
        os = new ObjectOutputStream(soc.getOutputStream());
        os.writeObject(Stat.OK);
        return name;
    }
    
    private Socket soc;
}